package sa.Master.Server.Modules;

import sa.Master.common.Classes.LoginFailureException;
import sa.Master.common.Classes.SubjectNotFoundException;
import sa.Master.common.Classes.UserInfo;
import sa.Master.common.Functions.*;
import sa.Master.common.NetworkMessages.Login.LoginResponse;

import java.io.*;
import java.util.*;

import com.google.gson.*;
import com.google.gson.stream.*;


public class UserInfoManager {

    private HashMap<String, UserInfo> userData;
    private File userDataJson;
    
    /**
     * Constructor of UserInfoManager
     * 
     * Read "userData.json" containing user's passwords and groups information located in server Root directory.
     * 
     * @param serverRoot the root directory of the server
     * @throws FileNotFoundException on
     *      1. Server root directory not exist.
     *      2. No "userData.json" in server root directory.
     * @throws IOException on
     *      1. File or Format error while reading "userData.json"
     * @see {@code docs/jsons.md} Format of "userData.json"
     * @author whsu
     */
    // TODO userData.json documentation
    public UserInfoManager(File serverRoot) throws FileNotFoundException, IOException, JsonSyntaxException {

        this.userDataJson = new File(serverRoot, "userData.json");
        this.userData = new HashMap<String, UserInfo>();

        // Wrong with File userData.json
        if (!(this.userDataJson.exists()) || !(this.userDataJson.isFile())) {
            throw new FileNotFoundException(String.format("Login Module: %s: User Validation info not found.", this.userDataJson));
        } else if (!(this.userDataJson.canRead())) {
            throw new FileNotFoundException(String.format("Login Module: %s: File not readable.", this.userDataJson));
        } // else: the file can be successfully read

        Gson gson = new Gson();
        JsonReader reader = new JsonReader(new FileReader(this.userDataJson));

        try {
            reader.beginArray();
            while (reader.hasNext()) {
                UserInfo user = gson.fromJson(reader, UserInfo.class);

                // Sort the groups array, for Binary Search
                if (user.joinedGroups != null) {
                    Collections.sort(user.joinedGroups);
                }
                if (user.adminGroups != null) {
                    Collections.sort(user.adminGroups);
                }

                userData.put(user.userName, user);
            }
        } catch (JsonIOException | IOException e) {
            throw new IOException(String.format("Login Module: %s: File IO error.", this.userDataJson));
        } catch (JsonSyntaxException e) {
            throw new JsonSyntaxException(String.format("Login Module: %s: File IO error.", this.userDataJson));
        }

    }

    /**
     * Check if the userName and password corresponds.
     *
     * synchronize: avoid missing some of the addings to wrongLoginTrial.
     * 
     * @param userName user's name
     * @param passwordHashed user's password (hashed by SHA-256 for now)
     * @return {@code true} if corresponds.
     */
    public synchronized LoginResponse.ReturnStatus validate(String userName, String passwordHashed) {

        UserInfo userInfo = userData.get(userName);

        if (userInfo==null) {
            return LoginResponse.ReturnStatus.wrong_pwd;
        }

        // check if user is blocked
        if (userInfo.wrongLoginTrial>=5 && userInfo.banDue>System.currentTimeMillis()) {
            return LoginResponse.ReturnStatus.blocked;
        }

        // check if password is valid
        if (!(userInfo.passwordHashed.equals(passwordHashed))) {  // not valid
            userInfo.wrongLoginTrial++;
            if (userInfo.wrongLoginTrial>=5) {
                userInfo.banDue = System.currentTimeMillis() + 86400000;  // a new non-login day
            }
            return LoginResponse.ReturnStatus.wrong_pwd;
        } else {  // valid
            userInfo.wrongLoginTrial = 0;
            return LoginResponse.ReturnStatus.success;
        }

    }

    /**
     * Change password
     *
     * Return with no exception if user don't exist.
     * Just log to the console.
     */
    public void updatePassword(String userName, String newPasswordHashed, String oldPasswordHashed) throws LoginFailureException {

        UserInfo userInfo = userData.get(userName);

        if (this.validate(userName, oldPasswordHashed) != LoginResponse.ReturnStatus.success) {
            throw new LoginFailureException("update password: Old password incorrect.");
        }

        if (userInfo==null) {
            LoggingUtilities.syserr("update password: User %d don't exists.");
            return;
        }

        userInfo.passwordHashed = newPasswordHashed;

    }

    /**
     *
     */
    public synchronized void createGroup(String initialAdmin, String groupName) throws SubjectNotFoundException {

        UserInfo userInfo = userData.get(initialAdmin);
        if (userInfo==null) {
            throw new SubjectNotFoundException(String.format("User %s not found.", initialAdmin));
        } // else

        userInfo.adminGroups.add(groupName);
        userInfo.joinedGroups.add(groupName);

    }

    /*public synchronized void addGroupMember(String adminName, String beInvitedUser, String groupName) {

        UserInfo adminInfo = userData.get(adminName);
        UserInfo beInvitedUserInfo = userData.get(beInvitedUser);

        if (adminInfo==null) {
            throw new NotFoundException(String.format("User %s not found.", adminName));
        } else if (beInvitedUserInfo==null) {
            throw new NotFoundException(String.format("User %s not found.", beInvitedUser));
        } // else

        if (!(validateAdmin(adminName, groupName))) {
            throw new
        }

    }*/




    /**
     * Check if user is a member of the group. 
     * 
     * @param userName user's name
     * @param group group's name
     * @return {@code true} if user is in the group.
     */
    public boolean validateGroup(String userName, String group) {

        final UserInfo user = userData.get(userName);
        if (user==null) {
            return false;
        } else {
            return BinarySearch.indexOf(user.joinedGroups, group, true) != -1;
        }

    }

    /**
     * Check if user administrates the group.
     *
     * @param userName user's name
     * @param group group's name
     * @return {@code true} if user administrates the group.
     */
    public boolean validateAdmin(String userName, String group) {

        final UserInfo user = userData.get(userName);
        if (user==null) {
            return false;
        } else {
            return BinarySearch.indexOf(user.joinedGroups, group, true) != -1;
        }

    }

    /**
     * Get user's information.
     * 
     * This method will get only the userName, joinedGroups, adminGroups field,
     * Excluding passwords and wrong trials information.
     * 
     * @param userName user's name
     * @return A string array containing all the groups a user is in.
     * @see sa.Master.common.Classes.UserInfo#UserInfo(UserInfo other) 
     */
    public String getUserInfo(String userName) {

        UserInfo user = userData.get(userName);
        Gson gson = new Gson();
        
        if (user==null) {
            return null;
        } else {
            return gson.toJson(new UserInfo(user));
        }

    }






    /**
     * Persist userdata.
     *
     * Write all the user's information stored on Memory into userData.json file.
     */
    public class FilePersist implements Runnable {

        public synchronized void run() {

            try {
                // write json to a String first, in order to perform prettify operation
                StringWriter jsonStringGenerator = new StringWriter();
                JsonWriter jsonWriter = new JsonWriter(jsonStringGenerator);
                Gson gson = new Gson();

                // generate json string
                jsonWriter.beginArray();
                for (Map.Entry<String, UserInfo> entry: userData.entrySet()) {
                    UserInfo value = entry.getValue();
                    gson.toJson(value, UserInfo.class, jsonWriter);
                }
                jsonWriter.endArray();
                jsonWriter.flush();

                String prettifiedJson = JsonFormat.prettify(jsonStringGenerator.toString());

                // file operation
                FileWriter jsonFileWriter = new FileWriter(userDataJson);
                jsonFileWriter.write(prettifiedJson);
                jsonFileWriter.flush();

                // close streams
                jsonFileWriter.close();
                jsonWriter.close();
                jsonStringGenerator.close();

                LoggingUtilities.sysout("Server: Login Module: File permanence successful.");
            } catch (IOException e) {
                LoggingUtilities.syserr("Login Module: Permanence process error.");
            }

        }

    }

}
